﻿// HSS.ServiceModel.VirtualOperation.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       VirtualOperation.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   12/06/2010
// ----------------------------------------------------------------------------
namespace HSS.ServiceModel
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Globalization;
	using System.Linq;
	using System.Linq.Expressions;
	using System.Reflection;
	using System.ServiceModel;
	using System.ServiceModel.Channels;
	using System.Threading;
	using HSS.ServiceModel.Internal;
	#endregion

	#region VirtualOperation<TContract>
	/// <summary>
	/// The base VirtualOperation class.
	/// </summary>
	/// <typeparam name="TContract">The Type of the Operation.</typeparam>
#if !DEBUG
	[System.Diagnostics.DebuggerStepThroughAttribute()]
#endif
	public abstract class VirtualOperation<TContract> where TContract : class
	{
		#region Fields

		private readonly string serviceToken;
		private readonly EndpointAddress serviceEndpoint;
		private readonly IDefaultCommunicationTimeouts communicationTimeouts;
		private readonly Type factoryType;

		private bool isAborted;
		private bool initialized;
		private object channel;
		private Type channelType;
		private MethodInfo beginMethod;
		private MethodInfo endMethod;
		private dynamic factory;

		#endregion

		#region Delegates
		private static readonly AsyncCallback onAsyncCallCompleted = new AsyncCallback(VirtualOperation<TContract>.OnAsyncCallCompleted);
		private delegate object EndOperationDelegate(IAsyncResult result);
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="endpoint">The EndpointAddress of the operation.</param>
		/// <param name="token">The shared token.</param>
		protected VirtualOperation(Type factoryType, EndpointAddress endpoint, string token)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (null == endpoint)
				throw new ArgumentNullException("endpoint");
			if (string.IsNullOrEmpty(token))
				throw new ArgumentNullException("token");
			this.factoryType = factoryType;
			this.serviceEndpoint = endpoint;
			this.serviceToken = token;
		}
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="endpoint">The EndpointAddress of the operation.</param>
		/// <param name="token">The shared token.</param>
		/// <param name="timeouts">The optional communication timeouts.</param>
		protected VirtualOperation(Type factoryType, EndpointAddress endpoint, string token, IDefaultCommunicationTimeouts timeouts)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (null == endpoint)
				throw new ArgumentNullException("endpoint");
			if (string.IsNullOrEmpty(token))
				throw new ArgumentNullException("token");
			this.factoryType = factoryType;
			this.serviceEndpoint = endpoint;
			this.serviceToken = token;
			this.communicationTimeouts = timeouts;

		}
#if SILVERLIGHT
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="servicePath">The virtual path that is providing accecss to VirtualServices.</param>
		/// <param name="serviceName">The Type Name for the Service (the FullName of the server side Type including it's Namespace)</param>
		/// <param name="token">The shared token.</param>
		protected VirtualOperation(Type factoryType, string servicePath, string serviceName, string token)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (string.IsNullOrEmpty(servicePath))
				throw new ArgumentNullException("servicePath");
			if (string.IsNullOrEmpty(serviceName))
				throw new ArgumentNullException("serviceName");
			if (string.IsNullOrEmpty(token))
				throw new ArgumentNullException("token");
			this.factoryType = factoryType;
			this.serviceEndpoint = VirtualServiceHelper.GetEndpoint(servicePath, serviceName);
			this.serviceToken = token;
		}
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="servicePath">The virtual path that is providing accecss to VirtualServices.</param>
		/// <param name="serviceName">The Type Name for the Service (the FullName of the server side Type including it's Namespace)</param>
		/// <param name="token">The shared token.</param>
		/// <param name="timeouts">The optional communication timeouts.</param>
		protected VirtualOperation(Type factoryType, string servicePath, string serviceName, string token, IDefaultCommunicationTimeouts timeouts)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (string.IsNullOrEmpty(servicePath))
				throw new ArgumentNullException("servicePath");
			if (string.IsNullOrEmpty(serviceName))
				throw new ArgumentNullException("serviceName");
			if (string.IsNullOrEmpty(token))
				throw new ArgumentNullException("token");
			this.factoryType = factoryType;
			this.serviceEndpoint = VirtualServiceHelper.GetEndpoint(servicePath, serviceName);
			this.serviceToken = token;
			this.communicationTimeouts = timeouts;
		}
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="configuration">The <see cref="IChannelConfiguration"/> instance containing the Virtual Service configuration.</param>
		protected VirtualOperation(Type factoryType, IChannelConfiguration configuration)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (null == configuration)
				throw new ArgumentNullException("configuration");
			if (string.IsNullOrEmpty(configuration.ServicePath))
				throw new ArgumentException("configuration.ServicePath");
			if (string.IsNullOrEmpty(configuration.ServiceTypeName))
				throw new ArgumentException("configuration.ServiceTypeName");
			if (string.IsNullOrEmpty(configuration.ServiceToken))
				throw new ArgumentException("configuration.ServiceToken");
			this.factoryType = factoryType;
			this.serviceEndpoint = VirtualServiceHelper.GetEndpoint(configuration.ServicePath, configuration.ServiceTypeName);
			this.serviceToken = configuration.ServiceToken;
		}
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="factoryType">The service ChannelFactory Type.</param>
		/// <param name="configuration">The <see cref="IChannelConfiguration"/> instance containing the Virtual Service configuration.</param>
		/// <param name="timeouts">The optional communication timeouts.</param>
		protected VirtualOperation(Type factoryType, IChannelConfiguration configuration, IDefaultCommunicationTimeouts timeouts)
		{
			if (null == factoryType)
				throw new ArgumentNullException("factoryType");
			if (null == configuration)
				throw new ArgumentNullException("configuration");
			if (string.IsNullOrEmpty(configuration.ServicePath))
				throw new ArgumentException("configuration.ServicePath");
			if (string.IsNullOrEmpty(configuration.ServiceTypeName))
				throw new ArgumentException("configuration.ServiceTypeName");
			if (string.IsNullOrEmpty(configuration.ServiceToken))
				throw new ArgumentException("configuration.ServiceToken");
			this.factoryType = factoryType;
			this.serviceEndpoint = VirtualServiceHelper.GetEndpoint(configuration.ServicePath, configuration.ServiceTypeName);
			this.serviceToken = configuration.ServiceToken;
			this.communicationTimeouts = timeouts;
		}
#endif
		#endregion

		#region Properties
#if !SILVERLIGHT
		/// <summary>
		/// Gets or sets the UserName.
		/// </summary>
		public string UserName { get; set; }
		/// <summary>
		/// Gets or sets the Password.
		/// </summary>
		public string Password { get; set; }
#endif
		#endregion

		#region Methods

		/// <summary>
		/// Abort the current operation.
		/// </summary>
		public void Abort()
		{
			if (isAborted)
				return;
			isAborted = true;

			try
			{
				var chnl = this.channel as IChannel;
				if (chnl != null)
					chnl.Abort();
			}
			catch { }

			try
			{
				if (factory != null)
					factory.Abort();
			}
			catch { }

		}

		/// <summary>
		/// Invoke the desired operation.
		/// </summary>
		/// <param name="operation">A <see cref="MethodCallExpression"/>.</param>
		/// <param name="callback">The delegate to call when the async operation completes.</param>
		/// <param name="userState">Any user supplied state.</param>
		protected void BeginInvoke(MethodCallExpression operation, SendOrPostCallback callback, object userState)
		{
			var op = AsyncOperationManager.CreateOperation(userState);
			var asyncContext = new AsyncOperationContext(op, this.EndInvoke, callback);

			object results = null;
			Exception error = null;
			IAsyncResult result = null;
			try
			{
				this.OpenChannel(operation);
				var parameters = BuildParameters(operation, onAsyncCallCompleted, asyncContext);
				result = (IAsyncResult)this.beginMethod.Invoke(this.channel, parameters);
				if (result.CompletedSynchronously)
					results = this.EndInvoke(result);
			}
			catch (Exception ex)
			{
				if (IsFatal(ex))
					throw;
				error = ex;
			}

			if ((error != null) || result.CompletedSynchronously)
				CompleteAsyncCall(asyncContext, results, error);
		}
		private object EndInvoke(IAsyncResult result)
		{
			object results = null;
			try { results = this.endMethod.Invoke(this.channel, new object[] { result }); }
			finally { this.CloseChannel(); }
			return results;
		}

		private void OpenChannel(MethodCallExpression operation)
		{
			if (null == operation)
				throw new ArgumentNullException("operation");
			if (this.initialized)
				throw new NotSupportedException("Cannot execute a VirtualOperation more than once.");
			this.initialized = true;

#if !SILVERLIGHT
			var binding = VirtualServiceHelper.GetBinding(this.serviceEndpoint, this.serviceToken, this.UserName, this.Password);
#else
			var binding = VirtualServiceHelper.GetBinding(this.serviceEndpoint, this.serviceToken);
#endif
			if (null != this.communicationTimeouts)
			{
				binding.CloseTimeout = this.communicationTimeouts.CloseTimeout;
				binding.OpenTimeout = this.communicationTimeouts.OpenTimeout;
				binding.ReceiveTimeout = this.communicationTimeouts.ReceiveTimeout;
				binding.SendTimeout = this.communicationTimeouts.SendTimeout;
			}

			this.factory = Activator.CreateInstance(this.factoryType, binding, this.serviceEndpoint);
			this.channel = this.factory.CreateChannel();
			this.channelType = this.channel.GetType();

			beginMethod = GetBeginMethod(operation);
			endMethod = GetEndMethod(operation);
		}
		private void CloseChannel()
		{
			if (isAborted)
				return;
			if (null != this.channel)
				((IChannel)this.channel).Close();
			if (null != factory)
				factory.Close();
		}

		private static void OnAsyncCallCompleted(IAsyncResult result)
		{
			if (!result.CompletedSynchronously)
			{
				var asyncState = (AsyncOperationContext)result.AsyncState;
				Exception error = null;
				object results = null;
				try
				{
					results = asyncState.EndDelegate(result);
				}
				catch (TargetInvocationException tie)
				{
					if (null != tie.InnerException)
						error = tie.InnerException;
					else
						error = tie;
				}
				catch (Exception ex)
				{
					if (IsFatal(ex))
						throw;
					error = ex;
				}
				CompleteAsyncCall(asyncState, results, error);
			}
		}
		private static void CompleteAsyncCall(AsyncOperationContext context, object results, Exception error)
		{
			if (context.CompletionCallback != null)
			{
				var arg = new InvokeAsyncCompletedEventArgs(results, error, false, context.AsyncOperation.UserSuppliedState);
				context.AsyncOperation.PostOperationCompleted(context.CompletionCallback, arg);
			}
			else
			{
				context.AsyncOperation.OperationCompleted();
			}
		}

		private static bool IsFatal(Exception exception)
		{
			while (exception != null)
			{
				if ((exception is OutOfMemoryException) || (exception is AccessViolationException))
					return true;
				if (!(exception is TypeInitializationException))
					break;
				exception = exception.InnerException;
			}
			return false;
		}

		private MethodInfo GetBeginMethod(MethodCallExpression expression)
		{
			return this.GetMethod("Begin", expression.Method.Name);
		}
		private MethodInfo GetEndMethod(MethodCallExpression expression)
		{
			return this.GetMethod("End", expression.Method.Name);
		}
		private MethodInfo GetMethod(string prefix, string name)
		{
			return this.channelType.GetMethod(prefix + name);
		}

		private static object[] BuildParameters(MethodCallExpression expression, AsyncCallback callBack, object userState)
		{
			var result = expression.Arguments.Select(arg => Expression.Lambda(arg).Compile().DynamicInvoke()).ToList();

			result.Add(callBack);
			result.Add(userState);

			return result.ToArray();
		}

		#endregion

		#region InvokeAsyncCompletedEventArgs
		/// <summary>
		/// InvokeAsyncCompletedEventArgs
		/// </summary>
		protected class InvokeAsyncCompletedEventArgs : AsyncCompletedEventArgs
		{
			#region Constructors
			/// <summary>
			/// Initializes a new instance of the InvokeAsyncCompletedEventArgs class.
			/// </summary>
			/// <param name="results">The results of the operation.</param>
			/// <param name="error">Any error that occurred during the asynchronous operation.</param>
			/// <param name="cancelled">A value that indicates whether the asynchronous operation was canceled.</param>
			/// <param name="userState">The optional user-supplied state object that is used to identify the task that raised the MethodNameCompleted event.</param>
			internal InvokeAsyncCompletedEventArgs(object results, Exception error, bool cancelled, object userState)
				: base(error, cancelled, userState)
			{
				this.Results = results;
			}
			#endregion

			#region Properties
			/// <summary>
			/// Gets the operations results.
			/// </summary>
			public object Results
			{
				get;
				private set;
			}
			#endregion
		}
		#endregion

		#region AsyncOperationContext
		/// <summary>
		/// AsyncOperationContext
		/// </summary>
		private class AsyncOperationContext
		{
			#region Constructors
			/// <summary>
			/// Constructor
			/// </summary>
			/// <param name="asyncOperation">The current AsyncOperation.</param>
			/// <param name="endDelegate">The EndInvoke Delegate.</param>
			/// <param name="completionCallback">The Completion Delegate.</param>
			internal AsyncOperationContext(AsyncOperation asyncOperation, EndOperationDelegate endDelegate, SendOrPostCallback completionCallback)
			{
				this.AsyncOperation = asyncOperation;
				this.EndDelegate = endDelegate;
				this.CompletionCallback = completionCallback;
			}
			#endregion

			#region  Properties
			/// <summary>
			/// Gets the current AsyncOperation.
			/// </summary>
			internal AsyncOperation AsyncOperation
			{
				get;
				private set;
			}
			/// <summary>
			/// Gets the EndInvoke Delegate.
			/// </summary>
			internal EndOperationDelegate EndDelegate
			{
				get;
				private set;
			}
			/// <summary>
			/// Gets the Completion Delegate.
			/// </summary>
			internal SendOrPostCallback CompletionCallback
			{
				get;
				private set;
			}
			#endregion
		}
		#endregion
	}
	#endregion
}